跳到主要内容

DDD 持久化方案

在实践领域驱动设计(DDD)的过程中,设计好了领域对象之后,每次业务的操作只需针对这个领域对象进行更新。如下代码,每次更新都是 HumanAggregate 这个整体

type Head struct {
// ...
}

type Body struct {
// ...
}

type HumanAggregate struct {
Head
Body
}

这样建模的好处之一就是我们考虑的问题是一个整体,将零碎的点构建为一个整体对象,如果该对象的行为需要发生改变,只需要修改该对象本身就可以了,而不是代码散落在各处需要到处查找。

可以看下面这图,传统的三层架构向DDD架构演变,主要发生在逻辑层和数据库访问层

提示

领域模型和持久化模型要分离吗?

这里借用大佬的回答:

当我们做持久化的时候,问题来了,我们需要单独的持久化模型吗?或者说,领域模型和持久化模型要分离吗?当你做持久化时,是直接拿领域模型做持久化,还是转换成持久化模型后做数据库保存。我遇到很多团队在这个问题上有分歧。有的说我这个系统的领域模型和持久化模型基本是一样的,没必要分离。另一部分观点说,这两种模型的职责是不一样的,应该分离,这样它们才能够分别独立演进。这两种听起来都很有道理。怎么选择?

我认为,应该分离这两种模型。原因非常简单,如果不分离,你的领域模型必然要为了持久化而妥协。比如说,你在设计领域模型时,要考虑如何保存到数据库中。更为糟糕的是,你还要满足ORM框架的要求,你要有空的构造方法,还要加上各种Setter。当你妥协完了后,你如何确保值对象是只读的?当你的属性很容易就被Set方法改变时,你如何封装你的业务规则?所以,通常我们都需要把领域模型和持久化模型分离。

但是理想很丰满,现实很骨感,这种以整体对象来作为操作的基本单位的方式,在遇到数据持久化的时候就会存在并发问题,设想,每次以一个整体去操作数据,那么很容易出现数据覆盖的情况,所以这种情况就不可避免的使用锁,代价是锁粒度大,冲突多。

为了解决上述的问题,这里需要设计一套持久化方案,其核心就是监听领域模型的哪个字段变更了。

这里使用 GORM 框架来完成持久化模型与表之间的映射

编写初始化环境

因为这里只是编写持久化工具,所以就只涉及 app、domain 层和基础设施层(其它无关的内容直接忽略了),这里提供基础的 DDD 示例代码

执行 SQL 初始化好环境后执行

go run .

检查控制台:

可以发现默认的这种 Update 方法每次更新都会更新整个聚合,如何做到只更新变更的字段呢?即只执行

UPDATE `tb_leg` SET `is_burly`=true WHERE `body_id` = 1

进行改造

检查字段变更

如果要监听字段是否被更新,那取得聚合第一件事就是去备份原始数据,这样下次更新时只需去检查原始数据就知道哪个字段变更了

type Origin struct {
Origin interface{}
}

因为结构体可能会有各种变化,所以这里使用 interface{} 去存结构体的内容,并通过反射取各字段的内容,同时结构体内可能存在别的结构体,所以这里可以加一个 Tag 来标识是否需要递归处理。

如下,改造后的聚合结构体

// 聚合对象
type HumanAggregate struct {
Head `origin:"deep"`
Body `origin:"deep"`
Leg `origin:"deep"`
// 添加一个 Origin 来存原始数据
Origin
}

然后给这个 Origin 添加方法用于检查哪些字段是变更的

// RealGetModifiedFields 获取修改结构体和原始结构体中差异部分
func (origin *Origin) RealGetModifiedFields(modified interface{}) map[string]interface{} {
if origin.Origin == nil {
return nil
}
originStructElem := reflect.ValueOf(origin).Elem()
originElem := originStructElem.FieldByName("Origin").Elem()
modifiedElem := reflect.ValueOf(modified).Elem()

if originElem.Type() != modifiedElem.Type() {
panic(fmt.Sprintln("原始类型和修改类型不一致", originElem.Type(), modifiedElem.Type()))
}

modifiedColumnValue := origin.modifiedFields("", originElem, modifiedElem)

return modifiedColumnValue
}

func (origin *Origin) modifiedFields(parentFieldName string, originElem, modifiedElem reflect.Value) map[string]interface{} {
numField := originElem.NumField() // 取得字段数
modifiedColumnValue := make(map[string]interface{})

for i := 0; i < numField; i++ {
// 如果字段的 Tag 是 `origin:"-"` 则跳过
if originElem.Type().Field(i).Tag.Get("origin") == "-" {
continue
}
// 判断值是否可以做为 interface{} 类型读出
// 例如 结构体内私有变量 CanInterface == false
if !originElem.Field(i).CanInterface() {
continue
}
fieldName := originElem.Type().Field(i).Name

// 结构体是否需要深入处理
if originElem.Type().Field(i).Tag.Get("origin") == "deep" {
ne := originElem.Field(i)
me := modifiedElem.FieldByName(fieldName)
if ne.Kind() == reflect.Ptr {
ne = ne.Elem()
me = me.Elem()
}
// 递归结构体
if ne.Kind() == reflect.Struct {
// 这里实际上是把递归的结果也添加进来
origin.addToMap(modifiedColumnValue, origin.modifiedFields(origin.appendFieldName(parentFieldName, fieldName), ne, me))
}
}
// 比较值是否相等
modifiedFieldValue := modifiedElem.FieldByName(fieldName).Interface()
originFieldValue := originElem.Field(i).Interface()
if !reflect.DeepEqual(modifiedFieldValue, originFieldValue) {
modifiedColumnValue[origin.appendFieldName(parentFieldName, fieldName)] = modifiedFieldValue
}
}
return modifiedColumnValue
}

func (origin *Origin) appendFieldName(parentFieldName, fieldName string) string {
if parentFieldName == "" {
return fieldName
}
return parentFieldName + "." + fieldName
}

// 合并 Map
func (origin *Origin) addToMap(a map[string]interface{}, b map[string]interface{}) {
for k, v := range b {
a[k] = v
}
}

关联实体与数据库的字段

有了上面能够返回字段变更的工具,下面就是如何去更新这些字段,但是上面监听的字段是 domain 层的实体(Entity),如何关联到 repo 层的实体(Model)呢?

所以这里就需要再次绑定 Entity 与 Model 之间的映射,以及 Model 与数据库之间的映射

这里创建一个数据类型来存这两种关系

// modelRelation 存储 model 的映射关系
// (即:domain 实体与 Model的关系,Model 与数据库字段的关系)
type modelRelation struct {
// Field entity 对应的结构体名称 -> model 对应的结构体名称
Field map[string]string
// DBColumn model 结构体字段 -> 数据库表字段名称
DBColumn map[string]string
}

type relation struct {
// modelRelation entity -> model 关系映射
entityToModel map[reflect.Type]map[reflect.Type]modelRelation
}


// newEntityToModel 创建 entityToModel
func newEntityToModel() modelRelation {
return modelRelation{
Field: make(map[string]string),
DBColumn: make(map[string]string),
}
}


func newRelation() relation {
return relation{
entityToModel: make(map[reflect.Type]map[reflect.Type]modelRelation),
}
}

这里的 relation 用来表示 entity 与 model 关系映射(因为可能多个值对象映射一个实体)

再用一个全局变量来存它们的关系

// relationCache 内部全局变量
var relationCache = newRelation()

然后就是去绑定这些关系了

点开观看完整代码
const (
relationTagName = "relation"
gormTagName = "gorm"
gormColumnTagName = "column"
)

var namer = schema.NamingStrategy{}

// registerRelation 注册 model -> entity 的关联关系
// 一个 model 对应多个 entity
func (r *relation) registerRelation(model interface{}, entities ...interface{}) {
for _, entity := range entities {
modelType := reflect.TypeOf(model).Elem()
// 获取结构体各字段的名字(fieldMap)
entityType, fieldMap := relationCache.getStructNameAndFieldIndex(entity)

// 绑定关系
entityToModelRelationRes := relationCache.getModelRelation(modelType, entityType, fieldMap)

// 注册到 relationCache
if _, ok := relationCache.entityToModel[entityType]; !ok {
relationCache.entityToModel[entityType] = make(map[reflect.Type]modelRelation)
}
relationCache.entityToModel[entityType][modelType] = entityToModelRelationRes
}
}

// getModelRelation 绑定 Model 与 Entity 的关系,与 Model 与 数据库的关系
func (r *relation) getModelRelation(modelType reflect.Type, entityType reflect.Type, fieldMap map[string]bool) modelRelation {
entityToModelRelation := newEntityToModel()
entityName := entityType.Name()

for i := 0; i < modelType.NumField(); i++ {
modelFieldName := modelType.Field(i).Name
fieldTag := modelType.Field(i).Tag
// 取得形如: Map[ActivityAggregate => ChooseRoomConfig.ChooseRoomMode] 这样的字段映射
structField := r.flatTag(fieldTag, relationTagName, ",")

// 因为可能一个 model 字段绑定了多个 entity 字段,所以需要遍历 structField
for entityStruct, entityField := range structField {
if (entityStruct != "" && entityField == "") || (entityStruct == "" && entityField != "") {
panic("注册 model 与 entity 关系错误:tag 标签错误 " + entityStruct + " " + entityField)
}

if entityStruct != "" && entityField != "" && modelFieldName != "" && entityStruct == entityName {
if !fieldMap[entityField] {
panic("注册 model 与 entity 关系错误:entity字段不存在 " + entityName + "." + entityField)
}

// 绑定 gorm,即数据库与 model 的关系
dbTagMap := r.flatTag(fieldTag, gormTagName, ";")
// 判断使用使用了 gorm 的 column tag 绑定了数据库的字段,如果没有绑定则默认是 ToColumnName
if dbColumnName, ok := dbTagMap[gormColumnTagName]; ok {
entityToModelRelation.DBColumn[modelFieldName] = dbColumnName
} else {
entityToModelRelation.DBColumn[modelFieldName] = namer.ColumnName("", modelFieldName)
}

// 绑定 entity 与 model 的关系
entityToModelRelation.Field[entityField] = modelFieldName
}
}
}

return entityToModelRelation
}

// getStructNameAndFieldIndex 获取结构体各字段的名字
func (r *relation) getStructNameAndFieldIndex(entity interface{}) (reflect.Type, map[string]bool) {
structType := reflect.TypeOf(entity).Elem()
fieldMap := r.deepFlatField(structType, "")
return structType, fieldMap
}

// deepFlatField 获取实体字段时会默认递归,如下:
// struct Entity{A *A}
// struct A {Name string}
// 生成的字段为 A.Name
// @return 传入的结构体的字段名称的 Map
func (r *relation) deepFlatField(structType reflect.Type, parentFieldName string) map[string]bool {
fieldMap := make(map[string]bool)

for i := 0; i < structType.NumField(); i++ {
fieldName := structType.Field(i).Name
fk := r.appendFieldName(parentFieldName, fieldName)
// 递归把结构体里面的字段都塞进来
if structType.Field(i).Type.Kind() == reflect.Ptr {
st := structType.Field(i).Type.Elem()
if st.Kind() == reflect.Struct {
r.addToMap(fieldMap, r.deepFlatField(st, fk))
}
} else if structType.Field(i).Type.Kind() == reflect.Struct {
r.addToMap(fieldMap, r.deepFlatField(structType.Field(i).Type, fk))
}
// 把取得的字段名称存进入
fieldMap[fk] = true
}
return fieldMap
}

// 合并 Map
func (r *relation) addToMap(a map[string]bool, b map[string]bool) {
for k, v := range b {
a[k] = v
}
}

func (r *relation) appendFieldName(parentFieldName, fieldName string) string {
if parentFieldName == "" {
return fieldName
}
return parentFieldName + "." + fieldName
}

// flatTag 打平对应的字段的 tag(即返回 tag 中的 key-value 关系)
// 例如:`relation:"ActivityAggregate:ChooseRoomConfig.ChooseRoomMode"`
// Map[ActivityAggregate => ChooseRoomConfig.ChooseRoomMode]
// @param key 为对应需要解析的 tag 名称
func (r *relation) flatTag(tag reflect.StructTag, key, keyValSeparate string) map[string]string {
strTag := tag.Get(key) // 例如 `relation:"ActivitySequence:ActivityId"` 读取为 "ActivitySequence:ActivityId"
names := strings.Split(strTag, keyValSeparate)
structField := make(map[string]string)
for _, v := range names {
realTag := strings.Split(v, ":")
for i := 0; i < len(realTag)-1; i += 2 {
structField[realTag[i]] = realTag[i+1]
}
}
if key == relationTagName && strTag != "" && len(structField) <= 0 {
panic("注册 model 与 entity 关系, tag 标签错误 " + strTag)
}

return structField
}

最后暴露一个方法用来注册关系

// RegisterRelation 绑定 model 与 domain 层的实体
func RegisterRelation(model interface{}, entities ...interface{}) {
relationCache.registerRelation(model, entities...)
}

转成数据库字段变更

上面把 Entity 的关系和 Model,Model 与数据库字段的关系绑定之后,那就需要把第一步检测到的更新字段转换成 model 改动数据库字段。

这一步也很简单,就是根据上面注册的关系找到对应的数据库字段就行了

// toDBColumn entity 对应修改的 字段,转换成想要的 model 改动数据库字段(key=>value)
func (r *relation) toDBColumn(entity interface{}, modifiedColumnValue map[string]interface{}, model interface{}) map[string]interface{} {
modelType := reflect.TypeOf(model).Elem()
entityType := reflect.TypeOf(entity).Elem()

if _, ok := r.entityToModel[entityType]; !ok {
panic("转换DB列名时没有找到注册的 entity")
}

entityToModelRelation, ok := r.entityToModel[entityType][modelType]
if !ok {
panic("转换DB列名时没有找到注册的 model")
}

updateColumns := make(map[string]interface{})
// 转db 字段
for entityField, v := range modifiedColumnValue {
modelField, ok := entityToModelRelation.Field[entityField]
if !ok {
continue
}
dbColumn, ok := entityToModelRelation.DBColumn[modelField]
if !ok {
continue
}

updateColumns[dbColumn] = v
}

return updateColumns
}

同样暴露一个方法用来使用

// ToDBColumn 把 entity 对应修改的字段,转换成想要的 model 改动数据库字段(key => value)
func ToDBColumn(entity interface{}, modifiedColumnValue map[string]interface{}, model interface{}) map[string]interface{} {
return relationCache.toDBColumn(entity, modifiedColumnValue, model)
}

改造后的代码

改造后的 完整代码

func main() {
humanApp := app.NewHumanApp(context.TODO())
humanApp.SetEyesColor(&domain.EyesColorDTO{
BodyId: 1,
Color: "#ff0",
})
}

最后执行方法

可以看到这里只执行了

UPDATE `tb_head` SET `eyes_color`='#ff0' WHERE `body_id` = 1

References